package com.service;

import com.alibaba.fastjson.JSON;
import com.common.RedisKeyPrefixConst;
import com.common.RedisUtil;
import com.dao.ProductDao;
import com.model.Product;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@Service
public class ProductService {
    @Autowired
    private ProductDao productDao;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private Redisson redisson;

    public static final Integer PRODUCT_CACHE_TIMEOUT = 60 * 60 * 24;
    public static final String EMPTY_CACHE = "{}";
    public static final String LOCK_PRODUCT_HOT_CACHE_PREFIX = "lock:product:hot_cache:";
    public static final String LOCK_PRODUCT_UPDATE_PREFIX = "lock:product:update:";
    public static Map<String, Product> productMap = new ConcurrentHashMap<>();

    public Product create(Product productParam) {
        Product product = productDao.create(productParam);
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE+product.getId(), JSON.toJSONString(product), genProductCacheTimeout(), TimeUnit.SECONDS);
        return product;
    }

    public Product update(Product productParam) {
        Product product = null;
        // 双写不一致解决方案
//        RLock updateLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + product);
//        updateLock.lock();
        RReadWriteLock readWriteLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + product);
        readWriteLock.readLock();
        try {
            product = productDao.update(productParam);
            redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE+product.getId(), JSON.toJSONString(product), genProductCacheTimeout(), TimeUnit.SECONDS);
            productMap.put(LOCK_PRODUCT_UPDATE_PREFIX+product.getId(), product);
        } finally {
//            updateLock.unlock();
            readWriteLock.unlink();
        }
        return product;
    }

    /**
     * 缓存雪崩解决方案：缓存雪崩是指大量缓存在同一时间内失效，此时恰好有大量请求来访问这些数据，由于缓存中没有，导致大量请求进入数据库。给所有 key 都加上随机过期时间
     *
     * 冷热分离：为防止商品常驻内存越来越大，实现冷热分离效果，每次将读过的数据延长过期时间。
     *
     * 缓存穿透解决方案：为防止每次缓存中查不到，都去查数据库，给数据库增加压力，可以给从数据库查到为null的数据缓存起来，设置短期过期时间（几分钟，防止数据库有了这个数据，但是缓存中没有更新），
     * 以及防止黑客换不同的空商品请求攻击，可能会导致上百万的空商品缓存在内存，占用大量内存，所以需要设置短期过期时间，同时如果黑客同时攻击同一个空商品，也可以给这个空商品每次访问都延期过期时间。
     *
     * 缓存击穿解决方案：在同一时间内，有大量请求查询同一个key(可能失效了，或者缓存没有缓存过这个key)，如李佳琪，薇娅直播间带货冷门商品（缓存中不存在的商品），几十万粉丝同时抢购这个商品，会导致大量请求进入数据库查询。
     * 可以通过双重检测锁的方式实现（类似单例模式的双重检测锁），只允许一个获得锁请求去数据库查询，将查询到的结果放入缓存，当其他请求再次获得锁会先查询缓存，此时缓存中是有值的，会直接返回给客户端，将不会查询数据库了。
     *
     * 双写不一致解决方案：如一个线程先从数据库读取了数据，准备更新缓存，但是在更新缓存之前卡了一下，卡的期间，另一个线程执行update方法更新更新了缓存，然后之前的线程又恢复了，更新了缓存，这样缓存中的数据就是旧数据。
     * 可以在get方法和update方法分别加锁实现，这样就不会有插队更新的问题了。
     *
     * 读写锁性能优化方案：读方法(get)加读锁，写方法(update)加写锁
     *
     * 多级缓存性能优化方案：可以使用如HashMap等JVM内存来缓存，推荐使用Guava等缓存框架来充当JVM内存缓存，因为它有一些过期策略可以选择。
     * 但是JVM内存缓存在分布式下会有缓存数据不一致问题，可以使用如mq发消息，zookeeper等解决方案来同步缓存数据。尽管这样，还是会有延时问题，这时候就不要再想着再用分布式锁去解决延时问题了，这样会特别复杂，会过度设计了。
     * 真实生产环境中，一般会把JVM内存级别缓存单独做成一个缓存系统，它会使用分布式实时计算（大数据技术）来计算热点中的热点缓存。而且如果要用多级缓存，本来就不应该保证强一致性。
     *
     * @param productId
     * @return
     */
    public Product get(Long productId) {
        // 多级缓存解决方案（JVM内存）
        Product product = productMap.get(LOCK_PRODUCT_UPDATE_PREFIX + productId);
        if (product != null) {
            return product;
        }

        product = getFromCache(productId);
        if (product != null) {
            return product;
        }

        // 缓存击穿解决方案
        // 加分布式锁解决热点缓存并发重建问题
        RLock redissonLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_PREFIX + productId);
        redissonLock.lock();
        try {
            product = getFromCache(productId);
            if (product != null) {
                return product;
            }

            // 双写不一致解决方案
//            RLock updateLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + product);
//            updateLock.lock();
            RReadWriteLock readWriteLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + product);
            readWriteLock.readLock();
            try {
                product = productDao.get(productId);
                if (product != null) {
                    redisUtil.set(LOCK_PRODUCT_HOT_CACHE_PREFIX + product.getId(), JSON.toJSONString(product), genProductCacheTimeout(), TimeUnit.SECONDS);
                } else {
                    // 缓存穿透解决方案
                    redisUtil.set(LOCK_PRODUCT_HOT_CACHE_PREFIX + product.getId(), EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
                }
            } finally {
//                updateLock.unlock();
                readWriteLock.unlink();
            }
        } finally {
            redissonLock.unlock();
        }

        return product;
    }

    private Product getFromCache(Long productId) {
        Product product = null;
        String resultStr = redisUtil.get(LOCK_PRODUCT_HOT_CACHE_PREFIX + productId);
        if (!StringUtils.isEmpty(resultStr)) {
            // 冷热分离
            if (EMPTY_CACHE.equals(resultStr)) {
                // 读延长
                redisUtil.expire(RedisKeyPrefixConst.PRODUCT_CACHE+productId, genEmptyCacheTimeout(), TimeUnit.SECONDS);
                return new Product();
            }
            // 读延长
            redisUtil.expire(RedisKeyPrefixConst.PRODUCT_CACHE+productId, genProductCacheTimeout(), TimeUnit.SECONDS);
            product = JSON.parseObject(resultStr, Product.class);
        }
        return product;
    }

    private Integer genEmptyCacheTimeout() {
        return 60 + new Random().nextInt(30);
    }

    private Integer genProductCacheTimeout() {
        // 缓存雪崩解决方案
        return PRODUCT_CACHE_TIMEOUT + new Random().nextInt(5) * 60 * 60;
    }
}